media_pp\elements\sink\renderer\windows/d3d11_renderer.rs
1use std::sync::Arc;
2
3use crate::pp_log::{PpLog, pp_error, pp_info};
4use ffmpeg_next as ffmpeg;
5use thiserror::Error as ThisError;
6use windows::{
7 Win32::Graphics::{
8 Direct3D11::{ID3D11Device, ID3D11Texture2D},
9 Dxgi::Common::{DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_NV12},
10 },
11 core::Interface,
12};
13
14use crate::{
15 buffer::MediaBuffer,
16 control::ControlMsg,
17 element::{Element, ElementType, Sink, element_pp_log},
18 elements::{SubmitError, filter::decoder::d3d11va_decoder::d3d11va_texture},
19 error::Result,
20 pool::UnboundObjectPoolRef,
21};
22
23/// What [`D3d11Renderer`] needs from an actual DX11 window/rendering
24/// implementation — the D3D11 sibling of
25/// [`crate::elements::D3d12FrameRenderer`], deliberately **not** an impl of
26/// that trait (it's documented as inherently D3D12-only). Unlike the D3D12
27/// trait, neither submit method here takes a fence *or* a `keep_alive` —
28/// see [`crate::elements::D3d11Renderer`]'s own docs on why a single
29/// shared `ID3D11Device` needs no explicit GPU-side synchronization at
30/// all, and this doc comment's own note below on why lifetime-keeping is
31/// unnecessary too. Both paths here are zero-copy (no CPU-upload method
32/// the way `D3d12FrameRenderer::submit_yuv420p` is one): everything in
33/// this crate's D3D11 stack already produces GPU-resident `Pixel::D3D11`
34/// textures (see [`crate::elements::D3d11Upload`]/
35/// [`crate::elements::DxgiCaptureSource`]'s GPU capture mode/
36/// [`crate::elements::D3d11Decoder`]), so there's no CPU-side pixel data
37/// left to upload by the time a frame reaches here.
38///
39/// No `keep_alive` parameter (unlike `D3d12FrameRenderer::submit_nv12_texture`):
40/// D3D11's own COM+driver contract already defers actually freeing a
41/// resource's GPU memory until the GPU has finished any outstanding work
42/// that reads it, *regardless* of when the app-level reference count hits
43/// zero — this is precisely the abstraction D3D12 (deliberately) doesn't
44/// provide, which is why that side needs the caller to keep the source
45/// frame alive by hand via an explicit fence. Here, once
46/// `D3d11Renderer::submit_d3d11_frame`'s local `texture` clone (and
47/// whatever `Arc<UnboundObjectPoolRef<..>>` produced it) drops, the
48/// runtime — not this crate — is what keeps the actual texture memory
49/// valid for as long as the GPU still needs it.
50pub trait D3d11FrameRenderer: Send {
51 /// The `ID3D11Device` this implementation actually renders/submits
52 /// with. [`D3d11Renderer`] reads this once at construction to guard
53 /// every submit against a texture from a different device — same
54 /// reasoning as `D3d12Renderer`'s own device-mismatch guard.
55 fn device(&self) -> ID3D11Device;
56
57 /// `texture` is a plain packed-BGRA surface — from
58 /// [`crate::elements::DxgiCaptureSource`]'s GPU capture mode or
59 /// [`crate::elements::D3d11Upload`] fed a BGRA source. `array_index` is
60 /// always `0` for these producers (neither ever builds an array
61 /// texture) — see `submit_nv12_texture`'s own docs on why it's a
62 /// parameter here at all.
63 ///
64 /// # Safety
65 /// `texture` must be a valid `ID3D11Texture2D` on the same
66 /// `ID3D11Device` this renderer was created with, `DXGI_FORMAT_B8G8R8A8_UNORM`,
67 /// with `array_index < ` its `ArraySize`.
68 unsafe fn submit_bgra_texture(
69 &self,
70 texture: ID3D11Texture2D,
71 array_index: u32,
72 width: u32,
73 height: u32,
74 ) -> std::result::Result<(), SubmitError>;
75
76 /// `texture` is an NV12 surface — from [`crate::elements::D3d11Decoder`]
77 /// or [`crate::elements::D3d11Upload`] fed an NV12 source. `array_index`
78 /// is which slice of `texture` this frame actually is: libavcodec's own
79 /// D3D11VA hwaccel decode pools frames as slices of one shared **array**
80 /// texture (unlike `D3d11Upload`, which always builds a fresh
81 /// non-array, single-slice texture per frame — `array_index` is always
82 /// `0` there) — see `d3d11va_texture`'s own docs.
83 ///
84 /// # Safety
85 /// `texture` must be a valid `ID3D11Texture2D` on the same
86 /// `ID3D11Device` this renderer was created with, `DXGI_FORMAT_NV12`,
87 /// with `array_index < ` its `ArraySize`.
88 unsafe fn submit_nv12_texture(
89 &self,
90 texture: ID3D11Texture2D,
91 array_index: u32,
92 width: u32,
93 height: u32,
94 ) -> std::result::Result<(), SubmitError>;
95
96 fn resize(&self, width: u32, height: u32) -> std::result::Result<(), SubmitError>;
97}
98
99/// Errors specific to `D3d11Renderer`. Converts into the crate-wide `Error`
100/// via `?` (see [`crate::error::Error`]).
101#[derive(Debug, ThisError)]
102pub enum D3d11RendererError {
103 #[error("failed to submit frame: {0:?}")]
104 Submit(SubmitError),
105
106 #[error("failed to resize: {0:?}")]
107 Resize(SubmitError),
108
109 #[error("D3d11Renderer only handles Pixel::D3D11 frames, got {0:?}")]
110 UnsupportedFormat(ffmpeg::format::Pixel),
111
112 #[error(
113 "frame claimed the D3D11 pixel format but carries no texture — must \
114 come from D3d11Upload/D3d11Decoder/DxgiCaptureSource's GPU mode"
115 )]
116 InvalidD3d11Frame,
117
118 #[error(
119 "D3d11Renderer only draws DXGI_FORMAT_B8G8R8A8_UNORM or DXGI_FORMAT_NV12 textures, got {0:?}"
120 )]
121 UnsupportedTextureFormat(DXGI_FORMAT),
122
123 #[error(
124 "a Pixel::D3D11 frame's texture lives on a different ID3D11Device \
125 than this D3d11Renderer was created with — every D3D11 element in \
126 one pipeline must share exactly one device for zero-copy to be \
127 valid"
128 )]
129 DeviceMismatch,
130
131 #[error("D3D11 texture array index {index} is outside ArraySize {array_size}")]
132 InvalidArrayIndex { index: isize, array_size: u32 },
133
134 #[error("windows error: {0}")]
135 Windows(#[from] windows::core::Error),
136}
137
138/// Terminal sink that submits `Pixel::D3D11` video frames to a
139/// caller-supplied [`D3d11FrameRenderer`] — the D3D11 sibling of
140/// [`crate::elements::D3d12Renderer`]. Only built with the
141/// `d3d11` feature.
142///
143/// Every producer in this crate's D3D11 stack
144/// ([`crate::elements::D3d11Upload`], [`crate::elements::D3d11Decoder`],
145/// [`crate::elements::DxgiCaptureSource`]'s GPU capture mode) is meant to
146/// share **one** `ID3D11Device` (and its one immediate context) with
147/// whatever [`D3d11FrameRenderer`] impl this wraps. That single-context
148/// requirement is what makes zero-copy here need **no explicit fence**,
149/// unlike [`crate::elements::D3d12Renderer`]'s `submit_nv12_texture`
150/// (which needs one because the D3D12 decoder and renderer are genuinely
151/// different devices/queues with nothing else to serialize them): an
152/// `ID3D11Device` created without `D3D11_CREATE_DEVICE_SINGLETHREADED` has
153/// its immediate context auto-serialized by the runtime across threads,
154/// and as long as every element funnels its GPU commands through that one
155/// context, the driver executes them in submission order — no separate
156/// sync object needed. This only holds because everything shares the
157/// *same* context; a second `ID3D11Device` in the mix would need its own
158/// explicit synchronization, same as the D3D12 case.
159///
160/// Dispatches on the *texture's own* `DXGI_FORMAT` (via `GetDesc`), not on
161/// any extra tag carried by the frame. `D3d11Upload` and GPU screen capture
162/// wrap manually-created textures, while `D3d11Decoder` receives textures
163/// from FFmpeg's D3D11VA frame pool; reading the actual texture description
164/// gives all of those producer paths one reliable source of truth for the
165/// pixel layout.
166pub struct D3d11Renderer {
167 pp_log: PpLog,
168 name: Arc<str>,
169 inner: Box<dyn D3d11FrameRenderer>,
170 /// Captured once from `inner.device()` at construction — see
171 /// `D3d12Renderer`'s own `device` field docs for why (fetched from
172 /// `inner` itself rather than a separate constructor parameter).
173 device: ID3D11Device,
174}
175
176impl D3d11Renderer {
177 /// `renderer` is whatever the caller's own [`D3d11FrameRenderer`]
178 /// implementation is — already constructed and pointed at a real
179 /// window/device by the time it gets here.
180 pub fn new(name: impl Into<String>, renderer: Box<dyn D3d11FrameRenderer>) -> Self {
181 let name: Arc<str> = name.into().into();
182 let pp_log = element_pp_log(ElementType::D3d11Renderer, &name, None);
183 pp_info!(pp_log: &pp_log, "created");
184 let device = renderer.device();
185 Self {
186 name,
187 pp_log,
188 inner: renderer,
189 device,
190 }
191 }
192
193 /// Call when the target window resizes.
194 pub fn resize(&self, width: u32, height: u32) -> Result<()> {
195 self.inner
196 .resize(width, height)
197 .inspect_err(|error| pp_error!(self, "resize failed: {error:?}"))
198 .map_err(D3d11RendererError::Resize)?;
199 pp_info!(self, "resized: {width}x{height}");
200 Ok(())
201 }
202
203 fn submit_d3d11_frame(
204 &self,
205 frame: Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>,
206 ) -> Result<()> {
207 let (texture_raw, index) =
208 d3d11va_texture(&frame).ok_or(D3d11RendererError::InvalidD3d11Frame)?;
209 let width = frame.width();
210 let height = frame.height();
211
212 // Safety: `texture_raw` is a borrowed raw `ID3D11Texture2D*` —
213 // still owned by `frame`'s own buffer reference, not by us.
214 // `.clone()` (`AddRef`) gives us an independently ref-counted
215 // handle, valid for as long as we hold it.
216 let texture = unsafe {
217 ID3D11Texture2D::from_raw_borrowed(&texture_raw)
218 .expect("D3d11 frame's texture pointer must not be null")
219 .clone()
220 };
221
222 // Same reasoning as `D3d12Renderer::submit_d3d12_frame`'s own
223 // device check: the producer and `self.inner` are independent
224 // constructions that only *should* share a device by convention —
225 // verify it.
226 let texture_device = unsafe { texture.GetDevice() }.map_err(D3d11RendererError::from)?;
227 if texture_device.as_raw() != self.device.as_raw() {
228 return Err(D3d11RendererError::DeviceMismatch.into());
229 }
230
231 let mut desc = Default::default();
232 unsafe { texture.GetDesc(&mut desc) };
233 if index < 0 || index as u64 >= u64::from(desc.ArraySize) {
234 let error = D3d11RendererError::InvalidArrayIndex {
235 index,
236 array_size: desc.ArraySize,
237 };
238 pp_error!(self, "{error}");
239 return Err(error.into());
240 }
241 let array_index = index as u32;
242
243 // No `keep_alive` to pass through here — see `D3d11FrameRenderer`'s
244 // own docs on why D3D11's driver-deferred resource destruction
245 // makes that unnecessary, unlike `D3d12Renderer`. `frame` itself
246 // just drops normally at the end of this function.
247 match desc.Format {
248 DXGI_FORMAT_B8G8R8A8_UNORM => unsafe {
249 self.inner
250 .submit_bgra_texture(texture, array_index, width, height)
251 .map_err(D3d11RendererError::Submit)?;
252 },
253 DXGI_FORMAT_NV12 => unsafe {
254 self.inner
255 .submit_nv12_texture(texture, array_index, width, height)
256 .map_err(D3d11RendererError::Submit)?;
257 },
258 other => return Err(D3d11RendererError::UnsupportedTextureFormat(other).into()),
259 }
260 Ok(())
261 }
262}
263
264impl Element for D3d11Renderer {
265 fn name(&self) -> Arc<str> {
266 self.name.clone()
267 }
268
269 fn element_type(&self) -> ElementType {
270 ElementType::D3d11Renderer
271 }
272
273 fn pp_log(&self) -> &PpLog {
274 &self.pp_log
275 }
276
277 fn pp_log_mut(&mut self) -> &mut PpLog {
278 &mut self.pp_log
279 }
280}
281
282impl Sink for D3d11Renderer {
283 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
284 let MediaBuffer::Video(frame) = buf else {
285 return Ok(());
286 };
287
288 if frame.format() != ffmpeg::format::Pixel::D3D11 {
289 let format = frame.format();
290 pp_error!(self, "unsupported pixel format: {format:?}");
291 return Err(D3d11RendererError::UnsupportedFormat(format).into());
292 }
293 self.submit_d3d11_frame(frame)
294 .inspect_err(|error| pp_error!(self, "submit_d3d11_frame failed: {error}"))
295 }
296
297 fn control(&mut self, _msg: ControlMsg) -> Result<()> {
298 // Terminal, nothing to flush or forward — same reasoning as
299 // `D3d12Renderer::control`.
300 Ok(())
301 }
302}